<?php
class SimplePKC
{
	const PADDING = ' ';
	
	public static function GeneratePrime ($length)
	{
		static $limb = 0;
		
		if ($limb == 0)
		{
			$rand = gmp_random(1);
			for ($i = 0; $i < 10; $i++)
			{
			  $limb = pow(2,$i);
			  if (gmp_scan1($rand, $limb) == -1) break;
			}
		}
		
		if ($length < $limb)
		{
			$rand = mt_rand(0,pow(2,$length));
			$rand = gmp_init($rand);
		}
		else $rand = gmp_random(floor($length/$limb));
        
		gmp_setbit($rand, $length);
		$prime = gmp_nextprime($rand);
		
		return gmp_strval($prime);
	}
	
	public static function GenerateKeyPair ($length)
	{
		$p = self::GeneratePrime($length/2+3);
		$q = self::GeneratePrime($length/2+3);
		$tot = gmp_mul(gmp_sub($p,1),gmp_sub($q,1));
		$mod = gmp_mul($p,$q);
		
		if ($length >= 16) $length = 16;
		else $length -= 2;
		
		do $e = self::GeneratePrime($length);
			while (gmp_strval(gmp_gcd($e,$tot)) != 1);
		$d = gmp_invert($e,$tot);
		
		$mod = gmp_strval($mod);
		$e = gmp_strval($e);
		$d = gmp_strval($d);
		
		if (gmp_cmp($e,$d) >= 0)
			list($e, $d) = array($d, $e);
		return array
		(
			'public' => array($mod, $e),
			'private' => array($mod, $d, $p, $q)
		);
	}
	
	public static function Encrypt ($message, $mod, $key = 0, $p = 0, $q = 0)
	{
		if (is_array($mod))
		{
			if (isset($mod[3]))
			{
				$q = $mod[3];
				$p = $mod[2];
			}
			$key = $mod[1];
			$mod = $mod[0];
		}
		
		$pad = 0; $mul = array(1,256); $i = 0;
		do {$pad++; $i++; $mul[] = gmp_mul($mul[$i],256);}
			while (gmp_cmp($mul[$i+1],$mod) <= 0);
		
		if ($p !== 0 && gmp_cmp($key,$p) > 0)
		{
			$dp = gmp_mod($key,gmp_sub($p,1));
			$dq = gmp_mod($key,gmp_sub($q,1));
			$qInv = gmp_invert($q,$p);
		}
		else $p = $q = 0;
		
		$message = (string) $message;
		$len = strlen($message);
		$remainder = $len % $pad;
		if ($remainder != 0) $message .= str_repeat(self::PADDING, $pad - $remainder);
		
		$transformed = '';
		for ($i = 0; $i < $len; $i += $pad)
		{
			$number = '';
			$string = substr($message, $i, $pad);
			for ($j = 0; $j < $pad; $j++) $number .= str_pad(decbin(ord($string[$j])),8,'0',STR_PAD_LEFT);
			$number = gmp_init($number,2);
			if ($p !== 0)
			{
				$number1 = gmp_powm($number,$dp,$p);
				$number2 = gmp_powm($number,$dq,$q);
				$h = gmp_mod(gmp_mul($qInv,gmp_add($p,gmp_sub($number1,$number2))),$p);
				$number = (gmp_add($number2,gmp_mul($h,$q)));
			}
			else $number = gmp_powm($number, $key, $mod);
			for ($j = 0; $j <= $pad; $j++)
			{
				$tmp = gmp_div_qr($number,$mul[$pad-$j]);
				$transformed .= chr(gmp_strval($tmp[0]));
				$number = $tmp[1];
			}
		}
		
		for ($i = strlen($transformed)-1; $i >= 0; $i--)
			if ($transformed[$i] != self::PADDING) break;
		$transformed = substr($transformed, 0, $i+1);
		
		return $transformed;
	}
	
	public static function Decrypt ($message, $mod, $key = 0, $p = 0, $q = 0)
	{
		if (is_array($mod))
		{
			if (isset($mod[3]))
			{
				$q = $mod[3];
				$p = $mod[2];
			}
			$key = $mod[1];
			$mod = $mod[0];
		}
		
		$pad = 1; $mul = array(1,256); $i = 0;
		do {$pad++; $i++; $mul[] = gmp_mul($mul[$i],256);}
			while (gmp_cmp($mul[$i+1],$mod) <= 0);
		
		if ($p !== 0 && gmp_cmp($key,$p) > 0)
		{
			$dp = gmp_mod($key,gmp_sub($p,1));
			$dq = gmp_mod($key,gmp_sub($q,1));
			$qInv = gmp_invert($q,$p);
		}
		else $p = $q = 0;
		
		$message = (string) $message;
		$len = strlen($message);
		$remainder = $len % $pad;
		if ($remainder != 0) $message .= str_repeat(self::PADDING, $pad - $remainder);
		
		$transformed = '';
		for ($i = 0; $i < $len; $i += $pad)
		{
			$number = '';
			$string = substr($message, $i, $pad);
			for ($j = 0; $j < $pad; $j++) $number .= str_pad(decbin(ord($string[$j])),8,'0',STR_PAD_LEFT);
			$number = gmp_init($number,2);
			if ($p !== 0)
			{
				$number1 = gmp_powm($number,$dp,$p);
				$number2 = gmp_powm($number,$dq,$q);
				$h = gmp_mod(gmp_mul($qInv,gmp_add($p,gmp_sub($number1,$number2))),$p);
				$number = (gmp_add($number2,gmp_mul($h,$q)));
			}
			else $number = gmp_powm($number, $key, $mod);
			for ($j = 2; $j <= $pad; $j++)
			{
				$tmp = gmp_div_qr($number,$mul[$pad-$j]);
				$transformed .= chr(gmp_strval($tmp[0]));
				$number = $tmp[1];
			}
		}
		
		for ($i = strlen($transformed)-1; $i >= 0; $i--)
			if ($transformed[$i] != self::PADDING) break;
		$transformed = substr($transformed, 0, $i+1);
		
		return $transformed;
	}
	
	static function NumberToBinary ($number)
	{
		$pos = 0;
		$posx = 0;
		$cache = '';
		$transformed = '';
		$func = 'gmp_scan1';
		if (gettype($number) != 'resource')
			$number = gmp_init($number);
		while ($pos != -1)
		{
			$cache .= str_repeat($func == 'gmp_scan1'?'0':'1',$pos - $posx);
			
			$posx = $pos;
			if ($func == 'gmp_scan1')
				$func = 'gmp_scan0';
			else $func = 'gmp_scan1';
			$pos = $func($number,$pos);
		}
		
		$len = strlen($cache);
		$remainder = $len % 8;
		if ($remainder > 0) $cache .= str_repeat('0',8 - $remainder);
		
		$cache = strrev($cache);
		for ($i = 0; $i < $len; $i += 8)
			$transformed .= chr(bindec(substr($cache,$i,8)));
		
		return $transformed;
	}
	
	static function BinaryToNumber ($data)
	{
		$number = '';
		$len = strlen($data);
		for ($i = 0; $i < $len; $i++) $number .= str_pad(decbin(ord($data[$i])),8,'0',STR_PAD_LEFT);
		$number = gmp_init($number,2);
		
		return gmp_strval($number);
	}
	
	static function PackKey ($mod, $exp = 0, $p = 0, $q = 0)
	{
		if (is_array($mod))
		{
			if (isset($mod[3]))
			{
				$q = $mod[3];
				$p = $mod[2];
			}
			$exp = $mod[1];
			$mod = $mod[0];
		}
		
		$mod = self::NumberToBinary($mod);
		$exp = self::NumberToBinary($exp);
		$mod = base64_encode($mod);
		$exp = base64_encode($exp);
		
		$return = "$mod:$exp";
		
		if ($p != 0)
		{
			$p = self::NumberToBinary($p);
			$q = self::NumberToBinary($q);
			$p = base64_encode($p);
			$q = base64_encode($q);
			
			$return .= ":$p:$q";
		}
		
		return $return;
	}
	
	static function UnpackKey ($data)
	{
		$data = explode(':', $data);
		$mod = self::BinaryToNumber(base64_decode($data[0]));
		$exp = self::BinaryToNumber(base64_decode($data[1]));
		
		$return = array($mod,$exp);
		
		if (isset($data[2]))
		{
			$p = self::BinaryToNumber(base64_decode($data[2]));
			$q = self::BinaryToNumber(base64_decode($data[3]));
			array_push($return,$p,$q);
		}
		
		return $return;
	}
}
